/* eslint-disable max-classes-per-file */

import StringBuf from './string-buf';
import { stringToArrayBuffer, nop } from '../utils/utils';
import Buffer from '@ohos.buffer';
import { LogUtil } from '../utils/log-util'

/**
 * 合并arraybuffer数组
 * @param arrayBuffers
 * @returns
 */
function combineArrayBuffers(arrayBuffers) {
    // 计算多个ArrayBuffer的总字节大小
    let totalByteLength = 0;
    for (const arrayBuffer of arrayBuffers) {
        totalByteLength += arrayBuffer.byteLength;
    }

    // 创建一个新的ArrayBuffer
    const combinedArrayBuffer = new ArrayBuffer(totalByteLength);

    // 创建一个Uint8Array来操作新的ArrayBuffer
    const combinedUint8Array = new Uint8Array(combinedArrayBuffer);

    // 依次复制每个ArrayBuffer的内容到新的ArrayBuffer中
    let offset = 0;
    for (const arrayBuffer of arrayBuffers) {
        const sourceUint8Array = new Uint8Array(arrayBuffer);
        combinedUint8Array.set(sourceUint8Array, offset);
        offset += sourceUint8Array.length;
    }

    return combinedArrayBuffer;
}

// data chunks - encapsulating incoming data
class StringChunk {
    constructor(data, encoding) {
        this._data = data;
        this._encoding = encoding;
    }

    get length() {
        return this.toBuffer().byteLength;
    }

    // copy to target buffer
    copy(target, targetOffset, offset, length) {
        let chunk = this.toBuffer().slice(offset, length - offset);
        return combineArrayBuffers([target.slice(targetOffset), chunk]);
    }

    toBuffer() {
        if (!this._buffer) {
            if (typeof this._data === 'string' || this._data instanceof String) {
                this._buffer = stringToArrayBuffer(this._data);
            } else {
                this._buffer = this._data;
            }
        }
        return this._buffer;
    }
}

class StringBufChunk {
    constructor(data) {
        this._data = data;
    }

    get length() {
        return this._data.length;
    }

    // copy to target buffer
    copy(target, targetOffset, offset, length) {
        // eslint-disable-next-line no-underscore-dangle
        return this._data._buf.copy(target, targetOffset, offset, length);
    }

    toBuffer() {
        return this._data.toBuffer();
    }
}

class BufferChunk {
    constructor(data) {
        this._data = data;
    }

    get length() {
        return this._data.length;
    }

    // copy to target buffer
    copy(target, targetOffset, offset, length) {
        this._data.copy(target, targetOffset, offset, length);
    }

    toBuffer() {
        return this._data;
    }
}


export default class StreamBuf {
    constructor(option) {
        this.option = {};
        this.buffers = [];
        this.batch = false;
        this.corked = false;
        this.pipes = [];
        this.paused = false;
        this.encoding = null;
    }

    toBuffer() {
        switch (this.buffers.length) {
            case 0:
                return null;
            case 1:
                return this.buffers[0];
            default:
                return combineArrayBuffers(this.buffers);
        }
    }

    async _pipe(chunk) {
        const write = function (pipe) {
            return new Promise(resolve => {
                pipe?.write(chunk.toBuffer(), () => {
                    resolve();
                });
            });
        };
        await Promise.all(this.pipes.map(write));
    }


    async write(data, encoding, callback) {
        if (encoding instanceof Function) {
            callback = encoding;
            encoding = 'utf8';
        }
        callback = callback || nop;

        // encapsulate data into a chunk
        let chunk;
        if (data instanceof StringBuf) {
            chunk = new StringBufChunk(data);
        } else if (data instanceof Buffer.Buffer) {
            chunk = new BufferChunk(data);
        } else if (typeof data === 'string' || data instanceof String || data instanceof ArrayBuffer) {
            chunk = new StringChunk(data, encoding);
        } else {
            throw new Error('Chunk must be one of type String, Buffer or StringBuf.');
        }

        // now, do something with the chunk
        if (this.pipes.length) {
            if (this.batch) {
                this.buffers.push(chunk.toBuffer());
                while (!this.corked && this.buffers.length > 1) {
                    this._pipe(this.buffers.shift());
                }
            } else if (!this.corked) {
                await this._pipe(chunk);
                callback();
            } else {
                this.buffers.push(chunk.toBuffer());
                callback();
            }
        } else {
            this.buffers.push(chunk.toBuffer());

        }

        return true;
    }


    cork() {
        this.corked = true;
    }


    _flush( /* destination */) {
        // if we have comsumers...
        if (this.pipes.length) {
            // and there's stuff not written
            while (this.buffers.length) {
                this._pipe(this.buffers.shift());
            }
        }
    }


    uncork() {
        this.corked = false;
        this._flush();
    }


    end(chunk, encoding, callback) {
        try {

            const writeComplete = error => {
                if (error) {
                    callback(error);
                } else {
                    this._flush();
                    this.pipes.forEach(pipe => {
                        pipe?.end();
                    });
                }
            };
            if (chunk) {

                this.write(chunk, encoding, writeComplete);
            } else {
                writeComplete();
            }

        } catch (e) {
            LogUtil.error("end error:" + JSON.stringify(e));
        }
    }


    read() {
        return combineArrayBuffers(this.buffers);
    }


    setEncoding(encoding) {
        // causes stream.read or stream.on('data) to return strings of encoding instead of Buffer objects
        this.encoding = encoding;
    }


    pause() {
        this.paused = true;
    }


    resume() {
        this.paused = false;
    }


    isPaused() {
        return !!this.paused;
    }


    pipe(destination) {
        // add destination to pipe list & write current buffer
        this.pipes.push(destination);
        if (!this.paused && this.buffers.length) {
            this.end();
        }
    }


    unpipe(destination) {
        // remove destination from pipe list
        this.pipes = this.pipes.filter(pipe => pipe !== destination);
    }


    unshift( /* chunk */) {
        // some numpty has read some data that's not for them and they want to put it back!
        // Might implement this some day
        throw new Error('Not Implemented');
    }


    wrap( /* stream */) {
        // not implemented
        throw new Error('Not Implemented');
    }
}
